Completed
Push — master ( f38440...342820 )
by Reetesh
39s
created

config.js ➔ ensureObject

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
nc 1
dl 0
loc 5
nop 1
1
'use strict'
2
3
let
4
  fs = require('fs')
5
6
class Config {
7
8
  constructor(file) {
9
    if(!fs.existsSync(file)) {
10
      throw new Error('Config: the file ' + file + ' does not exist')
11
    }
12
    this.content = JSON.parse(fs.readFileSync(file, 'utf8'))
13
  }
14
15
  get(path, key, input) {
16
    let obj = atPath(this.content, path)
17
    return evalKeyForValue(key, obj, input)
18
  }
19
20
  validate(path, key, input) {
21
    let value = this.get(path, key, input)
22
    if(undefined === value) {
23
      return false
24
    }
25
    if(false === validateRestrict(this, path, key, value, input)) {
26
      return false
27
    }
28
    return validateByType(key, value, input)
29
  }
30
31
}
32
33
function atPath(content, path) {
34
  if(!path) {
35
    throw new Error('Config: a non-empty path must be specified')
36
  }
37
  let components = path.split('.'), obj = content
38
  components.forEach(component => {
39
    if(!(component in obj)) {
40
      throw new Error('Config: component "' + component + '" in path "' + path + '" not found')
41
    }
42
    obj = obj[component]
43
  })
44
  return obj
45
}
46
47
function evalKeyForValue(key, obj, input) {
48
  ensureKey(key, obj)
49
  let value = obj[key]
50
  switch(getType(value)) {
51
    case 'scalar':
52
    case 'set':
53
    case 'expression':
54
    case 'keyword':
55
      return value
56
    case 'alias':
57
      return evalKeyForValue(value, obj, input)
58
    default: //case 'rule':
59
      return evalRuleForValue(value, input)
60
  }
61
}
62
63
function getType(val) {
64
  switch(typeof(val)) {
65
    case 'string':
66
      return parseStringType(val)
67
    case 'object':
68
      return parseObjectType(val)
69
    default:
70
      return 'scalar'
71
  }
72
}
73
74
function parseStringType(val) {
75
  if(val.match(/^#/)) {
76
    return 'alias'
77
  }
78
  if(val.match(/^<[a-z]+>$/)) {
79
    return 'keyword'
80
  }
81
  if(val.match(/^[<>!]/)) {
82
    return 'expression'
83
  }
84
  return 'scalar'
85
}
86
87
function parseObjectType(val) {
88
  return (isArray(val)) ? 'set' : 'rule'
89
}
90
91
function evalRuleForValue(rule, input) {
92
  if("@if" in rule && false === ensureIf(rule["@if"], input)) {
93
    return undefined
94
  }
95
  let value
96
  if("@key" in rule) {
97
    value = evalKeyDirectiveForValue(rule["@key"], rule, input)
98
  }
99
  return undefined === value ? rule["@values"] : value
1 ignored issue
show
Bug introduced by
The variable value does not seem to be initialized in case "@key" in rule on line 96 is false. Are you sure this can never be the case?
Loading history...
100
}
101
102
function ensureIf(rule, input) {
103
  ensureObject(input)
104
  ensureStatementFormat(rule)
105
  let key = Object.keys(rule)[0], keyType = getIfKeyType(key), value = rule[key]
106
  switch(keyType) {
107
    case 'operator':
108
      return evalCondition(key, value, input)
109
    default: // scalar
110
      return evalStatement(key, value, rule, input)
111
  }
112
}
113
114
function getIfKeyType(key) {
115
  return -1 !== ['&&', '||'].indexOf(key) ? 'operator' : 'scalar'
116
}
117
118
function evalCondition(operator, operands, input) {
119
  ensureOperandsFormat(operands, operator)
120
  switch(operator) {
121
    case '&&':
122
      return evalAndOperator(operands, input)
123
    default: // ||
124
      return evalOrOperator(operands, input)
125
  }
126
}
127
128
function evalAndOperator(operands, input) {
129
  for(let idx = 0; idx < operands.length; ++idx) {
130
    ensureStatementFormat(operands[idx])
131
    let statement = operands[idx], key = Object.keys(statement)[0], value = statement[key]
132
    if('operator' !== getIfKeyType(key)) {
133
      if(!evalStatement(key, value, statement, input)) {
134
        return false
135
      }
136
    } else {
137
      if(!evalCondition(key, value, input)) {
138
        return false
139
      }
140
    }
141
  }
142
  return true
143
}
144
145
function evalOrOperator(operands, input) {
146
  for(let idx = 0; idx < operands.length; ++idx) {
147
    ensureStatementFormat(operands[idx])
148
    let statement = operands[idx], key = Object.keys(statement)[0], value = statement[key]
149
    if('operator' !== getIfKeyType(key)) {
150
      if(evalStatement(key, value, statement, input)) {
151
        return true
152
      }
153
    } else {
154
      if(evalCondition(key, value, input)) {
155
        return true
156
      }
157
    }
158
  }
159
  return false
160
}
161
162
function evalStatement(key, value, rule, input) {
163
  switch(getType(value)) {
164
    case 'scalar':
165
      ensureKey(key, input)
166
      return matchScalar(key, value, input)
167
    case 'set':
168
      ensureKey(key, input)
169
      return matchSet(key, value, input)
170
    case 'expression':
171
      ensureKey(key, input)
172
      return matchExpr(key, value, input)
173
    case 'keyword':
174
      ensureKey(key, input)
175
      return matchKeyword(key, value, input)
176
    case 'alias':
177
      throw new Error('Config: malformed conditional statement with an alias "' + value + '" as value for key "' + key + '" in rule ' + JSON.stringify(rule))
178
    default: //case 'rule'
179
      return evalStatement(key, evalRuleForValue(value, input), rule, input)
180
  }
181
}
182
183
function matchScalar(key, scalar, input) {
184
  return (scalar === input[key])
185
}
186
187
function matchSet(key, set, input) {
188
  return (-1 !== set.indexOf(input[key]))
189
}
190
191
function matchExpr(key, expr, input) {
192
  /* eslint-disable no-eval */
193
  return (true === eval(makeEvalFriendly(input[key]) + ' ' + expr))
1 ignored issue
show
Security Performance introduced by
Calls to eval are slow and potentially dangerous, especially on untrusted code. Please consider whether there is another way to achieve your goal.
Loading history...
194
  /* eslint-enable no-eval */
195
}
196
197
function makeEvalFriendly(value) {
198
  if('string' === typeof(value)) {
199
    return "\'" + value + "\'"
200
  }
201
  return value
202
}
203
204
function matchKeyword(key, keyword, input) {
205
  let type = keyword.replace(/^</, '').replace(/>$/, '')
206
  return (typeof(input[key]) === type)
207
}
208
209
function evalKeyDirectiveForValue(key, rule, input) {
210
  if(!key) {
211
    throw new Error('Config: invalid value for "@key" in ' + JSON.stringify(rule))
212
  }
213
  ensureObject(input)
214
  ensureKey(key, input)
215
  if(!(input[key] in rule)) {
216
    // This is expected as several times we'll do the key lookups but won't find
217
    // them, and then we must go to the top most level and look for other means
218
    // to find the required value
219
    return undefined
220
  }
221
  return evalKeyForValue(input[key], rule, input)
222
}
223
224
function ensureKey(key, input) {
225
  if(!(key in input)) {
226
    throw new Error('Config: key "' + key + '" does not exist in ' + JSON.stringify(input))
227
  }
228
}
229
230
function validateRestrict(config, path, key, value, input) {
231
  let obj = atPath(config.content, path + '.' + key)
232
  if(hasRestrictKey(obj) && false === evalRestrict(key, value, input, obj["@restrict"])) {
233
    return false
234
  }
235
  return true
236
}
237
238
function hasRestrictKey(obj) {
239
  return ('object' === typeof(obj) && !isArray(obj) && "@restrict" in obj)
240
}
241
242
function evalRestrict(key, value, input, restrictRule) {
243
  let restrictValue = evalRestrictRule(input, restrictRule)
244
  if(undefined === restrictValue) {
245
    return true
246
  }
247
  switch(getType(restrictValue)) {
248
    case 'scalar':
249
      return matchScalar(key, restrictValue, input)
250
    case 'set':
251
      return matchSet(key, restrictValue, input)
252
    case 'expression':
253
      return matchExpr(key, restrictValue, input)
254
    default: // case 'keyword':
255
      return matchKeyword(key, restrictValue, input)
256
  }
257
}
258
259
function evalRestrictRule(input, restrictRule) {
260
  ensureObject(restrictRule)
261
  return evalRuleForValue(restrictRule, input)
262
}
263
264
function validateByType(key, value, input) {
265
  switch(getType(value)) {
266
    case 'scalar':
267
      return matchScalar(key, value, input)
268
    case 'set':
269
      return matchSet(key, value, input)
270
    case 'expression':
271
      return matchExpr(key, value, input)
272
    default: //case 'keyword':
273
      return matchKeyword(key, value, input)
274
  }
275
}
276
277
function ensureObject(val) {
278
  if(!val || 'object' !== typeof(val) || isArray(val)) {
279
    throw new Error('Config: invalid variable, an object expected but found ' + JSON.stringify(val))
280
  }
281
}
282
283
function ensureOperandsFormat(operands, operator) {
284
  if(!operands || 'object' !== typeof(operands) || !isArray(operands)) {
285
    throw new Error('Config: invalid set of operands "' + JSON.stringify(operands) + '" for "' + operator + '" operator, expected an array')
286
  }
287
}
288
289
function ensureStatementFormat(statement) {
290
  if(!statement || 'object' !== typeof(statement) || isArray(statement) || Object.keys(statement).length > 1) {
291
    throw new Error('Config: invalid statement "' + JSON.stringify(statement) + '", expected an object with exactly one key')
292
  }
293
}
294
295
function isArray(v) {
296
  return ('function' === typeof(v.indexOf))
297
}
298
299
exports.Config = Config
300